/**
 *  
 */
package com.googlecode.cswish.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * PageElement repository
 * <p>
 * It store all the element information to a list, each name of PageElement should be different
 * 
 * <p>Structure:
 * <pre>
 * 1. Element Tree:
 * PageElements
 *     |-- PageElement
 *     |-- PageElement
 *     |-- PageElements
 * 	       |-- PageElement
 * 	       |-- ...
 * 
 * 3. Form names:
 * model
 * model.element1
 * model.element2
 * model.element3.element31
 * 
 * 4. relationship
 * element.name == @cs.element.name == uiBean.name == @s.xxx.name
 * element.name != element.shortName
 * 
 * </pre>
 * 
 * @author Jerry.Feng Date: 2008-4-8
 */
public class PageElements extends PageElement {
	
	private static final Log logger = LogFactory.getLog(PageElements.class);
	
	public static final PageElements BLANK = new PageElements();
	
	private boolean disablePermissionCheck;
	
	private FreemarkerPageContext context;
	
	// child elements
	private Map<String, PageElement> childElements = new LinkedHashMap<String, PageElement>();

	public boolean isDisablePermissionCheck() {
		return disablePermissionCheck;
	}

	public void setDisablePermissionCheck(boolean disablePermissionCheck) {
		this.disablePermissionCheck = disablePermissionCheck;
	}

	public FreemarkerPageContext getContext() {
		return context;
	}
	
	public FreemarkerPageContext safeContext() {
		if (context == null) {
			context = new FreemarkerPageContext();
		}
		return context;
	}

	public void setContext(FreemarkerPageContext context) {
		this.context = context;
	}
	
	public PageElement newElement() {
		PageElement pageElement =  new PageElement();
		pageElement.setRequired(Boolean.FALSE);
		pageElement.setTextModel(Boolean.FALSE);
		pageElement.setHidden(Boolean.FALSE);
		return pageElement;
	}

	public void addElement(PageElement childElement) {
		addElement(childElement.getName(), childElement);
	}
	
	/**
	 * Hidden element isn't visible
	 *  
	 * @param target
	 * @param child element.shortName & element.visible are required
	 * @return
	 */	
	public void addElement(String name, PageElement childElement) {
		// cache it 
		PageElement existed = childElements.put(name, childElement);
		
		
		if (childElement.getParent() == null) {
			childElement.setParent(this);
		}
		// cache all the child element to parent
		if (childElement instanceof PageElements) {
			PageElements ps = (PageElements) childElement;
			childElements.putAll(ps.getAllElements());
		}
		
		if (existed != null && existed != childElement) {	// merge
			childElement.merge(existed);
			
			// notify the child reference container to use the new childElement
			updateChildElement(this, name, childElement);
		}
		
		if (this.parent != null) {
			((PageElements)parent).addElement(name, childElement);
		}
	}
	
	private void updateChildElement(PageElements ps, String name, PageElement element) {
		for (PageElement child : ps.childElements.values()) {
			if (child instanceof PageElements) {
				PageElements childCtn = (PageElements) child;
				PageElement childExisted = childCtn.getElement(name, false);
				if (childExisted != null && childExisted != element) {
					childCtn.childElements.put(name, element);		// update the element to child element
				}
				
				updateChildElement(childCtn, name, element);
			}
		}
	}
	
	public PageElement addElement(String type, String name, String label, boolean required, boolean textModel, int width, boolean show) {
		// TODO: fix the currency issue, it adds the duplicated PageElement
		
		if (getElement(name, false) != null) {
			logger.warn("element '" + name + "' existed");
		}
		
		PageElement element = new PageElement();
		element.setType(type);

		element.setName(name);
		element.setLabel(label);
		element.setRequired(required);
		element.setLength(width);
		element.setHidden(!show);

		if (textModel) {
			element.setTextModel(Boolean.TRUE);
		} else {
			element.setTextModel(Boolean.FALSE);
		}
		addElement(element);
		return element;
	}
	
	public void addHiddenElement(List<String> names) {
		for (String name : names) {
			if (getElement(name, false) == null) {
				PageElement element = new PageElement();
				element.setType("hidden");
				element.setName(name);
				element.setHidden(true);
				element.setTextModel(Boolean.FALSE);
				addElement(element);
			}
		}
	}
	
	public PageElements addElements(String type, String name) {
		PageElements elements = new PageElements();
		elements.setName(name);
		elements.setType(type);
		elements.setParent(this);
		addElement(elements);
		return elements;
	}
	
	public void removeElement(PageElement childElement) {
		removeElement(childElement.getName());
	}
	
	public PageElement removeElement(String name) {
		PageElement old = childElements.get(name);
		if (old != null) {
			// remove the child element if exists
			if (old instanceof PageElements) {
				PageElements ps = (PageElements) old;
				for (PageElement ele : ps.getAllElements().values()) {
					childElements.remove(ele.getName());
				}
			}
			childElements.remove(name);
			if (old.getParent() != null) {
				((PageElements)old.getParent()).removeElement(name);
			}
		}
		return old;
	}
	
	public PageElements checkAddElement(String shortName, String fullName, String type, Map<String, Object> exParameters) {
		PageElements element;
		if (fullName.length() > 0) {
			element = (PageElements) getElement(fullName, false);
			if (element == null) {
				element = addElements(type, fullName);
				if (this.isMock()) {
					element.addExtraParams("mock", Boolean.TRUE);
				}
				if (this.isDebug()) {
					element.addExtraParams("debug", Boolean.TRUE);
				}
				
				if (!exParameters.isEmpty()) {
					if (element.getExtraParams() == null) {
						element.setExtraParams(new HashMap<String, Object>(exParameters));
					} else {
						element.getExtraParams().putAll(exParameters);
					}
				}
				
				if (shortName != null && shortName.length() > 0) {
					if (getElement(shortName) == null) {
						childElements.put(shortName, element);
					}
				}
			}
		} else {
			element = this;
		}
		return element;
	}
	
	/**
	 * It supports the nested name.
	 * 
	 * search order: name as short name --> formated name as short name --> 
	 * skip the matched prefix and call child 
	 * 
	 * @param name: fullName/shortName
	 * @param showWarning
	 * @return
	 */
	public PageElement getElement(String childElementName, boolean showWarning) {
		PageElement element = childElements.get(childElementName);
		if (showWarning && element == null) {
			logger.debug("Can't find element:" + childElementName);
		}
		return element;
	}	

	/**
	 * Return a non-null object to avoid the unnecessary null object check
	 * @param name
	 * @return
	 */
	public PageElement safeElement(String name) {
		PageElement element = getElement(name, false);
		if (element == null) {
			element = new PageElement();
			element.setName(name);
			addElement(element);
		}
		return element;
	}
	
	public PageElement getElement(String name) {
		PageElement element = getElement(name, true);
		return element;
	}
	
	public PageElement findElementByType(String type) {
		for (PageElement pageElement : this.childElements.values()) {
			if (type.equals(pageElement.getType())) {
				return pageElement;
			}
		}
		return null;
	}
	
	public List<PageElement> findElementsByType(String type) {
		List<PageElement> list = new ArrayList<PageElement>();
		for (PageElement pageElement : this.childElements.values()) {
			if (type.equals(pageElement.getType())) {
				list.add(pageElement);
			}
		}
		return list;
	}
	
	public boolean containsType(String type) {
		for (PageElement pageElement : this.childElements.values()) {
			if (type.equals(pageElement.getType())) {
				return true;
			}
		}
		return false;
	}
	
	public Map<String, PageElement> getAllElements() {
		return childElements;
	}
	
	public List<PageElement> getIncludeElements() {
		List<PageElement> elements = new ArrayList<>();
		for (PageElement pageElement : getAllElements().values()) {
			String type = pageElement.getType();
			if ("include".equals(type)) {
				elements.add(pageElement);
			} else if ("script".equals(type) || "css".equals(type)) {
				Object src = pageElement.getExtraParam("src");
				if (src != null) {
					elements.add(pageElement);
				}
			}
		}
		return elements;
	}
	
	public Map<String, PageElement> getChildrenOfFirstLevel(String namePrefix, boolean onlyVisible) {
		Map<String, PageElement> firstLevelChildren = new LinkedHashMap<String, PageElement>();
		for (PageElement element : getAllElements().values()) {
			if (element.getParent() != this) {
				continue;
			}
			if (onlyVisible && (element.getHidden() != null && element.getHidden() || "hidden".equals(element.getType()))) {
				continue;
			}
			
			if (element.getName().startsWith(namePrefix)) {
				firstLevelChildren.put(element.getName(), element);
			}
		}
		return firstLevelChildren;
	}
	
	public Set<String> getEnableFields(String[] additionFields) {
		return getEnableFields(false, false, additionFields);
	}
	
	public Set<String> getEnableFields(boolean includeTextModelField, boolean useExactPath, String[] additionFields) {
		Set<String> enableFields = new HashSet<String>();
		Map<String, PageElement> elements = getAllElements();
		for (Entry<String, PageElement> elementEntry : elements.entrySet()) {
			PageElement element = elementEntry.getValue();
			// includeTextModelField or pageElement is not textModel
			if (!(element instanceof PageElements)  && 
					(includeTextModelField || element.isVisible() && (element.getTextModel() == null || !element.getTextModel()))) {
    			String fieldName = elementEntry.getKey();
    			addFieldName(fieldName, useExactPath, enableFields);
    			
    			Map<String, Object> extraParams = element.getExtraParams();
    			if (extraParams != null) {
    				String exNames = (String) extraParams.get(PageElement.EX_NAMES);
    				if (exNames != null) {
    					for (String exName : StringUtils.split(exNames, ',')) {
    						addFieldName(exName, useExactPath, enableFields);
    					}
    				}
    			}
			}
		}
		
		if (additionFields != null && additionFields.length > 0) {
			for (String additionField : additionFields) {
				addFieldName(additionField, useExactPath, enableFields);
			}
		}
		return enableFields;
	}
	
    private void addFieldName(String fieldName, boolean useExactPath, Set<String> enableFields) {
    	if (fieldName == null || fieldName.length() == 0) {
    		return ;
    	}
    	char firstChar = fieldName.charAt(0);
    	if (!Character.isLetter(firstChar)) {		// skip the invalid name
    		return;
    	}
    	
    	int begin = fieldName.indexOf('.') + 1;		// skip the first prefix: 'model.'
		int next = fieldName.length();
		do {
			String name = fieldName.substring(begin, next);
			if (!useExactPath) {
				// remove '['xxx']'
				int index = name.indexOf('[');
				if (index >= 0) {
					boolean skipModel = true;
					int len = name.length();
					StringBuilder sb = new StringBuilder(len);
					sb.append(name.substring(0, index));
					for (int i = index + 1; i < len; i++) {
						char c = name.charAt(i);
						if (c == '[') {
							skipModel = true;
						} else if (c == ']') {
							skipModel = false;
						} else {
    						if (!skipModel) {
    							sb.append(c);
    						}
						}
					}
					name = sb.toString();
				}
			}
			enableFields.add(name);
			next = fieldName.lastIndexOf('.', next - 1);
		} while (next > begin);
    }
	
    @SuppressWarnings({ "rawtypes", "unchecked" })
	@Override
	public PageElements clone() {
    	return deepClone(new IdentityHashMap());
	}
	
	// this --> cloned
    @Override
	protected PageElements deepClone(Map<PageElement, PageElement> identifyMap) {
    	PageElements cloned = new PageElements();
    	identifyMap.put(this, cloned);
    	this.cloneTo(cloned);
    	cloned.setDisablePermissionCheck(this.disablePermissionCheck);
    	cloned.setContext(this.context);
		cloned.setParent(identifyMap.get(this.getParent()));

		for (PageElement element : childElements.values()) {
			PageElement clonedChild = identifyMap.get(element);
			if (clonedChild == null) {
				clonedChild = element.deepClone(identifyMap);
				identifyMap.put(element, clonedChild);
				clonedChild.setParent(cloned);
			}
			cloned.childElements.put(clonedChild.getName(), clonedChild);
			
			// add to parent cache at the same time (Maybe, add to parent again)
			PageElement theParent = cloned.parent;
			while (theParent != null) {
				PageElements ps = (PageElements) theParent;
				ps.childElements.put(clonedChild.getName(), clonedChild);
				theParent = theParent.getParent();
			}
		}
		return cloned;
	}
	
	public void setElementId(String name , String id){
		PageElement pe  = getElement(name, false);
		if (pe != null){
			pe.setId(id);
		}
	}
	public void setElementLength(String name , int length){
		PageElement pe  = getElement(name, false);
		if (pe != null){
			if (pe.getLength() == null || pe.getLength().intValue() == 0){
				pe.setLength(length);
			}
		}
	}
	public void setElementClazz(String name , String clazz){
		PageElement pe  = getElement(name, false);
		if (pe != null){
			pe.setClazz(clazz);
		}
	}
	public void setElementVisible(String name , boolean visible){
		PageElement pe  = getElement(name, false);
		if (pe != null){
//			if (pe.getHidden() == null){
				pe.setHidden(!visible);
//			}
		}
	}
	public void setElementRequired(String name , boolean required){
		PageElement pe  = getElement(name, false);
		if (pe != null){
//			if (pe.getRequired() == null){
				pe.setRequired(required);
//			}
		}
	}
	
	public void setElementReadOnly(String name , boolean readOnly){
		PageElement pe  = getElement(name, false);
		if (pe != null){
//			if (pe.getTextModel() == null){
				if (readOnly){
					pe.setTextModel(Boolean.TRUE);
				} else {
					pe.setTextModel(Boolean.FALSE);
				}
//			}
		}
	}
	public void setElementColspan(String name , int colspan){
		PageElement pe  = getElement(name, false);
		if (pe != null){
			pe.setColspan(colspan);
		}
	}
	
	public void setGridAllowDblClick(String grid , boolean allow){
		PageElement pe = getElement(grid, false);
		if (pe != null){
			pe.addExtraParams("dblclick", allow);
		}
	}
	
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(this.name).append('=');
		for (PageElement element : getAllElements().values()) {
			sb.append(element).append(',');
		}
		
		return sb.toString();
	}
	
	public PageElements reset() {
		// TODO: don't reset the hidden/readonly fields, maybe the permission check disabled it
		if (parent == null) {
			childElements.clear();
		} else {
			for (String name : new ArrayList<String>(childElements.keySet())) {
				removeElement(name);
			}
		}
		
		if (this.context != null) {
			this.context = new FreemarkerPageContext();
		}
		return this;
	}
}